using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Magiq.Tests.Support;
using Magiq.Tests.TestModel;
using Magiq.Update;
using NUnit.Framework;

namespace Magiq.Tests
{
    public abstract class BaseMagiqProviderUpdatesTest<TTestClass> : BaseMagiqProviderTest<TTestClass>
        where TTestClass : class, ITestClass<TTestClass>, new()
    {
        [Test]
        public void ShouldUpdateStringProperties()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.StringProperty, "q"),
                       x => x.StringProperty == "q");
        }

        [Test]
        public void ShouldUpdateIntProperties()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.IntProperty, 3),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateReferencesAsValues()
        {
            var r = ReferenceClassInstances.First();
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference, r),
                       x => x.Reference.Id == r.Id);
        }

        [Test]
        public void ShouldUpdateChainedReferences()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.Reference.StringProperty, "lala"),
                       items => items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count(),
                       x => x.Reference != null && x.Reference.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateSelfReferences()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.SelfReference.StringProperty, "self changed"),
                       items =>
                       items.Where(x => x.SelfReference != null).Select(x => x.SelfReference).Distinct().Count(),
                       x => x.SelfReference != null && x.SelfReference.StringProperty == "self changed");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesSelfReference()
        {
            TestUpdate<TTestClass>(
                       x => x.SelfReference != null && x.SelfReference.IntProperty > 3,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateChainedReferencesWithOneSingleSetThatInvalidatesTheWhereClause()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, "lolo")
                                .Set(x => x.Reference.StringProperty, "lala"),
                       items =>
                       items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() + items.Count());
        }

        [Test]
        public void ShouldUpdateChainedReferencesWithSeveralSetsThatInvalidatesTheWhereClause()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa" && x.IntProperty == 3,
                       s => s.Set(x => x.StringProperty, "lolo")
                                .Set(x => x.IntProperty, 5)
                                .Set(x => x.Reference.StringProperty, "lala"),
                       items =>
                       items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() + items.Count());
        }

        [Test]
        public void ShouldUpdateReferencesAsExpressions()
        {
            var r = ReferenceClassInstances.First();
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference, x => r),
                       x => x.Reference.Id == r.Id);
        }


        [Test]
        public void ShouldUpdateReferenceEqualToSourceReference()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference, x => x.Reference),
                       x => true);
        }


        [Test]
        public void ShouldUpdateReferenceEqualToAnotherSourceReference()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference2, x => x.Reference),
                       x => true);
        }

        [Test]
        public void ShouldUpdateSeveralProperties()
        {
            var r = ReferenceClassInstances.First();
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.IntProperty, 3)
                                .Set(x => x.StringProperty, "lalala")
                                .Set(x => x.Reference, r),
                       x => x.Reference.Id == r.Id && x.IntProperty == 3 && x.StringProperty == "lalala");
        }

        [Test]
        public void ShouldUpdateSeveralEntities()
        {
            var items = TestClassInstances.ToList().Where(x => x.StringProperty == "sarasa");
            var count = items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() +
                        items.Count();
            TestClassInstances.Where(x => x.StringProperty == "sarasa")
                .Set(x => x.IntProperty, 3)
                .Set(x => x.AnotherStringProperty, "pe")
                .Set(x => x.Reference.StringProperty, "lala")
                .Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesStringProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateComplexWhere()
        {
            TestUpdate<TTestClass>(
                       x =>
                       (x.StringProperty == "sarasa" && x.IntProperty >= 3) ||
                       x.AnotherIntProperty < 12 && x.AnotherStringProperty != null,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesAnotherStringProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == x.AnotherStringProperty,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }


        [Test]
        public void ShouldUpdateWhereThatUsesStringPropertyNull()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == null,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesComposedStringProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == x.AnotherStringProperty + "la",
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWithComposedStringProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.StringProperty + "lala"),
                       x => x.StringProperty == "sarasalala");
        }

        [Test]
        public void ShouldUpdateWithComposedStringProperties()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.StringProperty + x.AnotherStringProperty),
                       x => x.StringProperty == "sarasa" + x.AnotherStringProperty);
        }

        [Test]
        public void ShouldUpdateFromAnotherStringProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.AnotherStringProperty),
                       x => x.StringProperty == x.AnotherStringProperty);
        }

        [Test]
        public void ShouldUpdateWithComposedIntProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.IntProperty + 5),
                       x => x.IntProperty == 8);
        }

        [Test]
        public void ShouldUpdateWithComposedIntProperties()
        {
            TestUpdate<TTestClass>(
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.IntProperty + x.AnotherIntProperty),
                       x => x.IntProperty == 3 + x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateFromAnotherIntProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.AnotherIntProperty),
                       x => x.IntProperty == x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateWhereThatUsesIntProperty()
        {
            var items = TestClassInstances.Where(x => x.IntProperty == 3);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesComposedIntProperty()
        {
            var items = TestClassInstances.Where(x => x.IntProperty == x.AnotherIntProperty + 3);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesAnotherIntProperty()
        {
            var items = TestClassInstances.Where(x => x.IntProperty == x.AnotherIntProperty);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesReference()
        {
            var r = ReferenceClassInstances.First();
            var items = TestClassInstances.Where(x => x.Reference == r);
            var count = items.Count();
            items.Set(x => x.Reference, r).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesReferenceNull()
        {
            var r = ReferenceClassInstances.First();
            var items = TestClassInstances.Where(x => x.Reference == null);
            var count = items.Count();
            items.Set(x => x.Reference, x => r).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateNullToReferenceAsExpressions()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference, x => null),
                       x => x.Reference == null);
        }

        [Test]
        public void ShouldUpdateNullToReferenceAsValues()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.Reference, (ReferenceTestClass)null),
                       x => x.Reference == null);
        }

        [Test]
        public void ShouldUpdateNullToStringProperty()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.StringProperty, x => null),
                       x => x.StringProperty == null);
        }

        [Test]
        public void ShouldUpdateNullToNullableIntProperty()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.NullableIntProperty, x => null),
                       x => x.NullableIntProperty == null);
        }

        [Test]
        public void ShouldUpdateLongToIntProperty()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.IntProperty, 3L),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateByteToIntProperty()
        {
            const byte value = 3;
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.IntProperty, value),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateNullableIntProperties()
        {
            TestUpdate<TTestClass>(
                       s => s.Set(x => x.NullableIntProperty, 3),
                       x => x.NullableIntProperty == 3);
        }

        [Test]
        public void ShouldUpdateWithComposedNullableIntProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.NullableIntProperty == 3,
                       s => s.Set(x => x.NullableIntProperty, x => x.NullableIntProperty + 5),
                       x => x.NullableIntProperty == 8);
        }

        [Test]
        public void ShouldUpdateWithComposedNullableIntProperties()
        {
            TestUpdate<TTestClass>(
                       x => x.NullableIntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.NullableIntProperty + x.AnotherIntProperty),
                       x => x.IntProperty == 3 + x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateNullableFromAnotherIntProperty()
        {
            TestUpdate<TTestClass>(
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.NullableIntProperty, x => x.AnotherIntProperty),
                       x => x.NullableIntProperty == x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntProperty()
        {
            var items = TestClassInstances.Where(x => x.NullableIntProperty == 3);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyNull()
        {
            var items = TestClassInstances.Where(x => x.NullableIntProperty == null);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyWithIntProperty()
        {
            var items = TestClassInstances.Where(x => x.NullableIntProperty == x.IntProperty);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyToNull()
        {
            var items = TestClassInstances.Where(x => x.NullableIntProperty == null);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        protected void TestUpdate<T>(Func<IQueryable<T>, IUpdate<T>> sets,
                                            Expression<Func<T, bool>> updatedWhere) where T : class
        {
            TestUpdate(x => true, sets, updatedWhere);
        }

        protected void TestUpdate<T>(Expression<Func<T, bool>> where,
                                            Func<IQueryable<T>, IUpdate<T>> sets, Expression<Func<T, bool>> updatedWhere) where T : class
        {
            TestUpdate(where, sets, items => items.Distinct().Count(), updatedWhere);
        }

        protected int TestUpdate<T>(Expression<Func<T, bool>> where,
                                           Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount) where T : class
        {
            var items = Configurator.GetQueryable<T>().Where(where);
            var count = getCount(items);
            sets(Configurator.GetQueryable<T>().Where(where)).Update().Should(Be.EqualTo(count));
            return count;
        }

        protected void TestUpdate<T>(Expression<Func<T, bool>> where,
                                            Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount,
                                            Expression<Func<T, bool>> updatedWhere) where T : class
        {
            var count = TestUpdate( where, sets, getCount);
            CheckPoint();
            Configurator.GetQueryable<T>().ToList().Where(updatedWhere.Compile()).Count().Should(Be.GreaterThanOrEqualTo(count));
        }
    }
}